import { gql } from '@urql/core';
import { it, expect, describe } from 'vitest';
import { __initAnd_query as query } from '../operations/query';
import { __initAnd_write as write } from '../operations/write';
import { Store } from '../store/store';
import { relayPagination } from './relayPagination';

function itemNode(numItem: number) {
  return {
    __typename: 'Item',
    id: numItem + '',
  };
}

function itemEdge(numItem: number) {
  return {
    __typename: 'ItemEdge',
    node: itemNode(numItem),
  };
}

describe('as resolver', () => {
  it('works with forward pagination', () => {
    const Pagination = gql`
      query ($cursor: String) {
        __typename
        items(first: 1, after: $cursor) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasNextPage
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          endCursor: '1',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          endCursor: null,
        },
      },
    };

    write(store, { query: Pagination, variables: { cursor: null } }, pageOne);
    write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo);

    const res = query(store, { query: Pagination });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageTwo,
      items: {
        ...pageTwo.items,
        edges: [pageOne.items.edges[0], pageTwo.items.edges[0]],
        nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]],
      },
    });
  });

  it('works with backwards pagination', () => {
    const Pagination = gql`
      query ($cursor: String) {
        __typename
        items(last: 1, before: $cursor) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            startCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasPreviousPage: true,
          startCursor: '2',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasPreviousPage: false,
          startCursor: null,
        },
      },
    };

    write(store, { query: Pagination, variables: { cursor: null } }, pageOne);
    write(store, { query: Pagination, variables: { cursor: '2' } }, pageTwo);

    const res = query(store, { query: Pagination });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageTwo,
      items: {
        ...pageTwo.items,
        edges: [pageTwo.items.edges[0], pageOne.items.edges[0]],
        nodes: [pageTwo.items.nodes[0], pageOne.items.nodes[0]],
      },
    });
  });

  it('handles duplicate edges', () => {
    const Pagination = gql`
      query ($cursor: String) {
        __typename
        items(first: 2, after: $cursor) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasNextPage
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1), itemEdge(2)],
        nodes: [itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          endCursor: '2',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2), itemEdge(3)],
        nodes: [itemNode(2), itemNode(3)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          endCursor: null,
        },
      },
    };

    write(store, { query: Pagination, variables: { cursor: null } }, pageOne);
    write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo);

    const res = query(store, { query: Pagination });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageTwo,
      items: {
        ...pageTwo.items,
        edges: [
          pageOne.items.edges[0],
          pageTwo.items.edges[0],
          pageTwo.items.edges[1],
        ],
        nodes: [
          pageOne.items.nodes[0],
          pageTwo.items.nodes[0],
          pageTwo.items.nodes[1],
        ],
      },
    });
  });

  it('works with simultaneous forward and backward pagination (outwards merging)', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination({ mergeMode: 'outwards' }),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: null,
          endCursor: '1',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: true,
          startCursor: '2',
          endCursor: '2',
        },
      },
    };

    const pageThree = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(-1)],
        nodes: [itemNode(-1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          hasPreviousPage: true,
          startCursor: '-1',
          endCursor: null,
        },
      },
    };

    write(
      store,
      { query: Pagination, variables: { after: '1', first: 1 } },
      pageOne
    );
    write(
      store,
      { query: Pagination, variables: { after: '2', first: 1 } },
      pageTwo
    );
    write(
      store,
      { query: Pagination, variables: { before: '1', last: 1 } },
      pageThree
    );

    const res = query(store, {
      query: Pagination,
      variables: { before: '1', last: 1 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageThree,
      items: {
        ...pageThree.items,
        edges: [
          pageThree.items.edges[0],
          pageOne.items.edges[0],
          pageTwo.items.edges[0],
        ],
        nodes: [
          pageThree.items.nodes[0],
          pageOne.items.nodes[0],
          pageTwo.items.nodes[0],
        ],
        pageInfo: {
          ...pageThree.items.pageInfo,
          hasPreviousPage: true,
          hasNextPage: true,
          startCursor: '-1',
          endCursor: '2',
        },
      },
    });
  });

  it('works with simultaneous forward and backward pagination (inwards merging)', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination({ mergeMode: 'inwards' }),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: null,
          endCursor: '1',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: true,
          startCursor: '2',
          endCursor: '2',
        },
      },
    };

    const pageThree = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(-1)],
        nodes: [itemNode(-1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          hasPreviousPage: true,
          startCursor: '-1',
          endCursor: null,
        },
      },
    };

    write(
      store,
      { query: Pagination, variables: { after: '1', first: 1 } },
      pageOne
    );
    write(
      store,
      { query: Pagination, variables: { after: '2', first: 1 } },
      pageTwo
    );
    write(
      store,
      { query: Pagination, variables: { before: '1', last: 1 } },
      pageThree
    );

    const res = query(store, {
      query: Pagination,
      variables: { before: '1', last: 1 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageThree,
      items: {
        ...pageThree.items,
        edges: [
          pageOne.items.edges[0],
          pageTwo.items.edges[0],
          pageThree.items.edges[0],
        ],
        nodes: [
          pageOne.items.nodes[0],
          pageTwo.items.nodes[0],
          pageThree.items.nodes[0],
        ],
        pageInfo: {
          ...pageThree.items.pageInfo,
          hasPreviousPage: true,
          hasNextPage: true,
          startCursor: '-1',
          endCursor: '2',
        },
      },
    });
  });

  it('prevents overlapping of pagination on different arguments', () => {
    const Pagination = gql`
      query ($filter: String) {
        items(first: 1, filter: $filter) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasNextPage
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const page = withId => ({
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(withId)],
        nodes: [itemNode(withId)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          endCursor: null,
        },
      },
    });

    write(
      store,
      { query: Pagination, variables: { filter: 'one' } },
      page('one')
    );
    write(
      store,
      { query: Pagination, variables: { filter: 'two' } },
      page('two')
    );

    const resOne = query(store, {
      query: Pagination,
      variables: { filter: 'one' },
    });
    const resTwo = query(store, {
      query: Pagination,
      variables: { filter: 'two' },
    });
    const resThree = query(store, {
      query: Pagination,
      variables: { filter: 'three' },
    });

    expect(resOne.data).toHaveProperty('items.edges[0].node.id', 'one');
    expect(resOne.data).toHaveProperty('items.edges.length', 1);

    expect(resTwo.data).toHaveProperty('items.edges[0].node.id', 'two');
    expect(resTwo.data).toHaveProperty('items.edges.length', 1);

    expect(resThree.data).toEqual(null);
  });

  it('returns an empty array of edges when the cache has zero edges stored', () => {
    const Pagination = gql`
      query {
        items(first: 1) {
          __typename
          edges {
            __typename
          }
          nodes {
            __typename
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    write(
      store,
      { query: Pagination },
      {
        __typename: 'Query',
        items: {
          __typename: 'ItemsConnection',
          edges: [],
          nodes: [],
        },
      }
    );

    const res = query(store, {
      query: Pagination,
    });

    expect(res.data).toHaveProperty('items', {
      __typename: 'ItemsConnection',
      edges: [],
      nodes: [],
    });
  });

  it('returns other fields on the same level as the edges', () => {
    const Pagination = gql`
      query {
        __typename
        items(first: 1) {
          __typename
          totalCount
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    write(
      store,
      { query: Pagination },
      {
        __typename: 'Query',
        items: {
          __typename: 'ItemsConnection',
          totalCount: 2,
        },
      }
    );

    const resOne = query(store, {
      query: Pagination,
    });

    expect(resOne.data).toHaveProperty('items', {
      __typename: 'ItemsConnection',
      totalCount: 2,
    });
  });

  it('returns a subset of the cached items if the query requests less items than the cached ones', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination({ mergeMode: 'outwards' }),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [
          itemEdge(1),
          itemEdge(2),
          itemEdge(3),
          itemEdge(4),
          itemEdge(5),
        ],
        nodes: [
          itemNode(1),
          itemNode(2),
          itemNode(3),
          itemNode(4),
          itemNode(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 2 } }, results);

    const res = query(store, {
      query: Pagination,
      variables: { first: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(results);
  });

  it("returns the cached items even if they don't fullfil the query", () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [
          itemEdge(1),
          itemEdge(2),
          itemEdge(3),
          itemEdge(4),
          itemEdge(5),
        ],
        nodes: [
          itemNode(1),
          itemNode(2),
          itemNode(3),
          itemNode(4),
          itemNode(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(
      store,
      { query: Pagination, variables: { after: '3', first: 3, last: 3 } },
      results
    );

    const res = query(store, {
      query: Pagination,
      variables: { after: '3', first: 3, last: 3 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(results);
  });

  it('returns the cached items even when they come from a different query', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [
          itemEdge(1),
          itemEdge(2),
          itemEdge(3),
          itemEdge(4),
          itemEdge(5),
        ],
        nodes: [
          itemNode(1),
          itemNode(2),
          itemNode(3),
          itemNode(4),
          itemNode(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 5 } }, results);

    const res = query(store, {
      query: Pagination,
      variables: { after: '3', first: 2, last: 2 },
    });

    expect(res.partial).toBe(true);
    expect(res.data).toEqual(results);
  });

  it('caches and retrieves correctly queries with inwards pagination', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [
          itemEdge(1),
          itemEdge(2),
          itemEdge(3),
          itemEdge(4),
          itemEdge(5),
        ],
        nodes: [
          itemNode(1),
          itemNode(2),
          itemNode(3),
          itemNode(4),
          itemNode(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(
      store,
      { query: Pagination, variables: { after: '2', first: 2, last: 2 } },
      results
    );

    const res = query(store, {
      query: Pagination,
      variables: { after: '2', first: 2, last: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(results);
  });

  it('does not include a previous result when adding parameters', () => {
    const Pagination = gql`
      query ($first: Int, $filter: String) {
        __typename
        items(first: $first, filter: $filter) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1), itemEdge(2)],
        nodes: [itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '2',
        },
      },
    };

    const results2 = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [],
        nodes: [],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '2',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 2 } }, results);

    write(
      store,
      { query: Pagination, variables: { first: 2, filter: 'b' } },
      results2
    );

    const res = query(store, {
      query: Pagination,
      variables: { first: 2, filter: 'b' },
    });
    expect(res.data).toEqual(results2);
  });

  it('Works with edges absent from query', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination({ mergeMode: 'outwards' }),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        nodes: [
          itemNode(1),
          itemNode(2),
          itemNode(3),
          itemNode(4),
          itemNode(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 2 } }, results);

    const res = query(store, {
      query: Pagination,
      variables: { first: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(results);
  });

  it('Works with nodes absent from query', () => {
    const Pagination = gql`
      query ($first: Int, $last: Int, $before: String, $after: String) {
        __typename
        items(first: $first, last: $last, before: $before, after: $after) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          pageInfo {
            __typename
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      schema: require('../test-utils/relayPagination_schema.json'),
      resolvers: {
        Query: {
          items: relayPagination({ mergeMode: 'outwards' }),
        },
      },
    });

    const results = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [
          itemEdge(1),
          itemEdge(2),
          itemEdge(3),
          itemEdge(4),
          itemEdge(5),
        ],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: '1',
          endCursor: '5',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 2 } }, results);

    const res = query(store, {
      query: Pagination,
      variables: { first: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(results);
  });

  it('handles subsequent queries with larger last values', () => {
    const Pagination = gql`
      query ($last: Int!) {
        __typename
        items(last: $last) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasPreviousPage
            startCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasPreviousPage: true,
          startCursor: '2',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1), itemEdge(2)],
        nodes: [itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasPreviousPage: false,
          startCursor: '1',
        },
      },
    };

    const pageThree = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(0), itemEdge(1), itemEdge(2)],
        nodes: [itemNode(0), itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasPreviousPage: false,
          startCursor: '0',
        },
      },
    };

    write(store, { query: Pagination, variables: { last: 1 } }, pageOne);
    write(store, { query: Pagination, variables: { last: 2 } }, pageTwo);

    let res = query(store, { query: Pagination, variables: { last: 2 } });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(pageTwo);

    write(store, { query: Pagination, variables: { last: 3 } }, pageThree);

    res = query(store, { query: Pagination, variables: { last: 3 } });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(pageThree);
  });

  it('handles subsequent queries with larger first values', () => {
    const Pagination = gql`
      query ($first: Int!) {
        __typename
        items(first: $first) {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasNextPage
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          endCursor: '1',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1), itemEdge(2)],
        nodes: [itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          endCursor: '2',
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 1 } }, pageOne);
    write(store, { query: Pagination, variables: { first: 2 } }, pageTwo);

    const res = query(store, { query: Pagination, variables: { first: 2 } });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(pageTwo);
  });

  it('ignores empty pages when paginating', () => {
    const PaginationForward = gql`
      query ($first: Int!, $after: String) {
        __typename
        items(first: $first, after: $after) {
          __typename
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            startCursor
            endCursor
          }
        }
      }
    `;
    const PaginationBackward = gql`
      query ($last: Int!, $before: String) {
        __typename
        items(last: $last, before: $before) {
          __typename
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const forwardOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        nodes: [itemNode(1), itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          startCursor: '1',
          endCursor: '2',
        },
      },
    };
    const forwardAfter = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        nodes: [],
        pageInfo: {
          __typename: 'PageInfo',
          startCursor: null,
          endCursor: null,
        },
      },
    };
    const backwardBefore = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        nodes: [],
        pageInfo: {
          __typename: 'PageInfo',
          startCursor: null,
          endCursor: null,
        },
      },
    };

    write(
      store,
      { query: PaginationForward, variables: { first: 2 } },
      forwardOne
    );
    write(
      store,
      { query: PaginationBackward, variables: { last: 1, before: '1' } },
      backwardBefore
    );

    const res = query(store, {
      query: PaginationForward,
      variables: { first: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(forwardOne);
    write(
      store,
      { query: PaginationForward, variables: { first: 1, after: '2' } },
      forwardAfter
    );

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(forwardOne);
  });

  it('allows for an empty page when this is the only result', () => {
    const Pagination = gql`
      query ($first: Int!, $after: String) {
        __typename
        items(first: $first, after: $after) {
          __typename
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            startCursor
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      resolvers: {
        Query: {
          items: relayPagination(),
        },
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        nodes: [],
        pageInfo: {
          __typename: 'PageInfo',
          startCursor: null,
          endCursor: null,
        },
      },
    };

    write(store, { query: Pagination, variables: { first: 2 } }, pageOne);
    const res = query(store, {
      query: Pagination,
      variables: { first: 2 },
    });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual(pageOne);
  });
});

describe('as directive', () => {
  it('works with forward pagination', () => {
    const Pagination = gql`
      query ($cursor: String) {
        __typename
        items(first: 1, after: $cursor) @_relayPagination {
          __typename
          edges {
            __typename
            node {
              __typename
              id
            }
          }
          nodes {
            __typename
            id
          }
          pageInfo {
            __typename
            hasNextPage
            endCursor
          }
        }
      }
    `;

    const store = new Store({
      directives: {
        relayPagination: () => relayPagination(),
      },
    });

    const pageOne = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(1)],
        nodes: [itemNode(1)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: true,
          endCursor: '1',
        },
      },
    };

    const pageTwo = {
      __typename: 'Query',
      items: {
        __typename: 'ItemsConnection',
        edges: [itemEdge(2)],
        nodes: [itemNode(2)],
        pageInfo: {
          __typename: 'PageInfo',
          hasNextPage: false,
          endCursor: null,
        },
      },
    };

    write(store, { query: Pagination, variables: { cursor: null } }, pageOne);
    write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo);

    const res = query(store, { query: Pagination });

    expect(res.partial).toBe(false);
    expect(res.data).toEqual({
      ...pageTwo,
      items: {
        ...pageTwo.items,
        edges: [pageOne.items.edges[0], pageTwo.items.edges[0]],
        nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]],
      },
    });
  });
});
